package utils

import (
	"encoding/json"
	"fmt"
	"github.com/gomodule/redigo/redis"
	"log"
	"lottery/comm"
	"lottery/conf"
	"lottery/datasource"
	"lottery/models"
	"lottery/services"
	"math"
	"time"
)

// 公开方法，从奖品池拿到奖品的库存数量
func GetGiftPoolNum(id int) int {
	// hash 结构，全量缓存。
	key := "gift_pool"
	cacheObj := datasource.InstanceCache()
	rs, err := redis.Int(cacheObj.Do("HGET", key, id))
	if err != nil {
		log.Println("prizedata.GetGiftPoolNum error = ", err)
		return 0
	} else {
		//num := comm.GetInt64(rs, 0)
		return rs
	}
}

// 私有方法， 奖品池中 商品数减一
func prizeServGift(id int) bool {
	key := "gift_pool"
	cacheObj := datasource.InstanceCache()
	// 注意： redis中的数值是可以成为负数的
	num, err := redis.Int(cacheObj.Do("HINCRBY", key, id, -1))
	if err != nil {
		log.Println("prizedata.prizeServGift HINCRBY error = ", err)
		return false
	}
	if num >= 0 {
		return true
	} else {
		return false
	}
}

// 发奖功能。 处理发奖是否成功， 返回一个 bool 值
func PrizeGift(id, leftNum int) bool {
	// 引入奖品池,  先对奖品池进行验证，通过后才走后序发奖流程
	ok := false
	// 1. redis 奖品池数据递减
	ok = prizeServGift(id)
	if ok {
		giftService := services.NewGiftService()
		// 2. 递减操作: 奖品池递减后，这里的数据库剩余量（和缓存中的），也要递减
		rows, err := giftService.DecrLeftNum(id, leftNum-1)
		if rows < 1 || err != nil { // 没更新成功
			log.Println("prizedata.PrizeGift giftService.DecrLeftNum error=",
				err, ", rows=", rows)
			return false
		}
	}
	return ok
}

// 优惠券类的发放
func PrizeCodeDiff(id int, codeService services.CodeService) string {
	return prizeServCodeDiff(id, codeService)
}

// 从数据库中拿到一个券（redis版本中，被上面的方法替代了）
func prizeLocalCodeDiff(id int, codeService services.CodeService) string {
	// 重复使用 utils 里定义的分布式锁, 保证后续对数据库的操作是线程安全的。
	// 首先需要构造一个 id值， 这个值不能和用户的 uid 重复
	lockUid := 0 - id - 100000000000 // 保证唯一的id锁住
	ok := LockLucky(lockUid)
	if ok {
		defer UnlockLucky(lockUid)
	} else {
		return ""
	}
	// 上一个codeId 不好找，默认填 0
	codeInfo := codeService.NextUsingCode(id, 0)
	if codeInfo != nil && codeInfo.Id > 0 {
		codeInfo.SysStatus = 2 // 发放出去
		codeInfo.SysUpdated = comm.NowUnix()
		codeService.Update(codeInfo, nil) // 指定为 nil， 不为空的值，就能更新进去
	} else {
		log.Println("prizedata.PrizeCodeDiff num codeInfo, gift_id=", id)
		return ""
	}
	// 找到下一个可用的编码
	// 更新处理
	return codeInfo.Code
}

// 从缓存中导入一个优惠券
func ImportCacheCodes(id int, code string) bool {
	// 构造一个 key，是和 gift 相关的
	key := fmt.Sprintf("gift_code_%d", id)
	cacheObj := datasource.InstanceCache()
	_, err := cacheObj.Do("SADD", key, code)
	if err != nil {
		log.Println("prizedata.ImportCacheCodes SADD error = ", err)
		return false
	} else {
		return true
	}
}

// 重新整理缓存中的优惠券， 保证mysql 和 redis 券的数据一致性
func RecacheCodes(id int, codeService services.CodeService) (sucNum, errNUm int) { // 成功和失败的次数
	// 1. mysql中 拿出所有的码
	// id: giftid
	list := codeService.Search(id, 0, math.MaxInt32) // 这里我给设置为分页了，应该一次返回所有匹配的数据，给个整形最大值吧
	if list == nil || len(list) <= 0 {
		return 0, 0
	}
	// 2. 创建临时 key， 导入有效的码
	key := fmt.Sprintf("gift_code_%d", id)
	cacheObj := datasource.InstanceCache()
	// 定义一个临时key
	tmpKey := "tmp_" + key
	for _, data := range list {
		// 只需要正常状态的。失效和已发放的不行
		if data.SysStatus == 0 {
			code := data.Code
			_, err := cacheObj.Do("SADD", tmpKey, code)
			if err != nil {
				log.Println("prizedata.RecacheCodes SADD error = ", err)
				errNUm++
			} else {
				sucNum++
			}
		}
	}
	// 3. 将临时的key 命名为正式的key
	// 关于重新定义键名的操作， 有三种情况：http://redisdoc.com/database/rename.html
	// newkey 已存在时， RENAME 会覆盖旧 newkey 的内容。
	_, err := cacheObj.Do("RENAME", tmpKey, key)
	if err != nil {
		log.Println("prizedata.RecacheCodes RENAME error = ", err)
	}
	return sucNum, errNUm
}

// 统计功能。当前奖品的优惠券总数， 缓存中剩余的优惠券数量
func GetCacheCodeNum(id int, codeService services.CodeService) (int, int) {
	var (
		num      int
		cacheNum int
	)
	// 这一点性能有问题。只需要数量，为啥还要读出全部数据再计算呢？ 直接数据库层面count就可以了。
	//list := codeService.Search(id, 0, math.MaxInt32)
	//if len(list) > 0 {
	//	// 只要合适的
	//	for _, data := range list {
	//		if data.SysStatus == 0 {
	//			num ++
	//		}
	//	}
	//}
	// 数据库中拿到数据
	num = codeService.SearchAvailableCodeCount(id)
	// redis 缓存中拿到数据
	key := fmt.Sprintf("gift_code_%d", id)
	cacheObj := datasource.InstanceCache()
	// SCARD 返回集合中元素的数量
	rs, err := cacheObj.Do("SCARD", key)
	if err != nil {
		log.Println("prizedata.RecacheCodes SCARD error = ", err)
	} else {
		cacheNum = int(comm.GetInt64(rs, 0))
	}
	return num, cacheNum
}

// 从缓存中拿到一个券
func prizeServCodeDiff(id int, codeService services.CodeService) string {
	key := fmt.Sprintf("gift_code_%d", id)
	cacheObj := datasource.InstanceCache()
	// 删除掉一个元素随机的，也就是随机拿出一个券
	re, err := cacheObj.Do("SPOP", key)
	if err != nil {
		log.Println("prizedate.prizeServCodeDiff SPOP error = ", err)
		return ""
	}
	code := comm.GetString(re, "")
	if code == "" {
		log.Println("prizedate.prizeServCodeDiff rs = %s", re)
		return ""
	}
	// 更新到数据库
	codeService.UpdateByCode(&models.LtCode{
		Code:       code,
		SysStatus:  2,
		SysUpdated: comm.NowUnix(),
	}, nil)
	return code
}

// 重新计算奖品的发奖计划数据
func ResetGiftPrizeData(giftInfo *models.LtGift, giftService services.GiftService) {
	// 参数验证
	if giftInfo == nil || giftInfo.Id < 1 {
		return
	}
	id := giftInfo.Id
	nowTime := comm.NowUnix()
	if giftInfo.SysStatus == 1 ||
		giftInfo.TimeBegin >= nowTime ||
		giftInfo.TimeEnd <= nowTime ||
		giftInfo.PrizeNum <= 0 {
		// 需要清空旧的发奖数据
		if giftInfo.PrizeData != "" {
			clearGiftPrizeData(giftInfo, giftService)
		}
		return
	}
	// 获取发奖周期
	dayNum := giftInfo.PrizeTime
	// 如果发奖周期为0， 那么就没有设置发奖周期，说明所有的奖品，一次性发完填充到奖品池中
	// 就不需要发奖计划了
	if dayNum <= 0 {
		setGiftPool(id, giftInfo.LeftNum)
		return
	}
	// 重置发奖计划数据
	// 首先需要清空奖品池，开启一轮新的发奖计划，当然要清空旧的发奖计划了。
	setGiftPool(id, 0)
	// 实际的奖品计划分布运算
	prizeNum := giftInfo.PrizeNum
	// 计算平均值
	avgNum := prizeNum / dayNum
	// 每天可以分配的奖品数: key：天数，value: 数量
	dayPrizeNum := make(map[int]int, 0)
	// 平均每天至少分配一个奖，才设置值
	if avgNum >= 1 {
		for day := 0; day < dayNum; day++ {
			dayPrizeNum[day] = avgNum
		}
	}
	// 不能整除，可能还有剩余， 剩下的就要随机分配了
	prizeNum -= dayNum * avgNum
	for prizeNum > 0 {
		prizeNum--
		day := comm.Random(dayNum)
		//_, ok := dayPrizeNum[day]
		//if !ok {
		//	dayPrizeNum[day] = 1
		//} else {
		//	dayPrizeNum[day] += 1
		//}
		// 可以直接简化为： 默认不存在就是 0
		dayPrizeNum[day] = dayPrizeNum[day] + 1
	}
	// 对每天的数据分配给每小时的map，60分钟的数组，奖品数
	prizeData := make(map[int]map[int][60]int) // 每天/每小时/每分钟
	for day, num := range dayPrizeNum {
		// 计算这一天的发奖计划
		dayPrizeData := getGiftPrizeDataOneDay(num)
		prizeData[day] = dayPrizeData
	}
	// 需要将周期内的每天，每小时，每分钟的数据 PrizeData 格式化（时间：数量）
	datalist := formatGiftPrizeData(nowTime, dayNum, prizeData)
	//fmt.Println(datalist)
	//fmt.Println(prizeData)
	// 更新入数据库
	str, err := json.Marshal(datalist)
	if err != nil {
		log.Println("prizedata.ResetGiftPrizeData json error = ", err)
	} else {
		// 更新几个重要数据
		info := &models.LtGift{
			Id:         giftInfo.Id,
			LeftNum:    giftInfo.LeftNum,
			PrizeData:  string(str),
			PrizeBegin: nowTime,
			PrizeEnd:   nowTime + dayNum*86400,
			SysUpdated: nowTime,
		}
		err := giftService.Update(info, nil)
		if err != nil {
			log.Println("prizedata.ResetGiftPrizeData giftService.Update",
				info, ", error=", err)
		}
	}
}

// 清空发奖数据: 将奖品池设置为 0
func clearGiftPrizeData(giftInfo *models.LtGift, giftService services.GiftService) {
	// 清空旧的发奖计划数据
	info := &models.LtGift{
		Id:        giftInfo.Id,
		PrizeData: "",
	}
	// 更新：// 这里需要强制指定一下,  因为空数据不指定是不会更新的
	err := giftService.Update(info, []string{"prize_data"})
	if err != nil {
		log.Println("prizedata.clearGiftPrizeData giftService.Update ",
			info, "error = ", err)
	}
	// 将奖品池设置为 0
	setGiftPool(giftInfo.Id, 0)
}

// 设置奖品池: 奖品id， 剩余数量
func setGiftPool(id int, num int) {
	// 设置奖品池的库存数量
	key := "gift_pool"
	cacheObj := datasource.InstanceCache()
	_, err := cacheObj.Do("HSET", key, id, num)
	if err != nil {
		log.Println("prizedata.setGiftPool error = ", err)
	}
}

// 计算出来一天的发奖计划: 将 num 数量的奖品发到 这一天里去
func getGiftPrizeDataOneDay(num int) map[int][60]int {
	// 定义结果集
	rs := make(map[int][60]int, 0)
	// 计算24小时，每小时对应的奖品数量
	hourData := [24]int{}
	// 如果 num > 100, 表示奖品数量很大，精确设置奖品数，剩余的再用随机方法算
	if num > 100 {
		// 统计一共分出去多少
		hourNum := 0
		// 每小时精确分配一遍，按照 PrizeDataRandomDayTime
		// 以 100 为单位，这里计算出来每小时得到几个（也就是概率）
		for _, h := range conf.PrizeDataRandomDayTime {
			hourData[h]++
		}
		// 开始分配
		for h := 0; h < 24; h++ {
			// 当前小时 以 100 为单位，可以获得几个
			chanceNum := hourData[h]
			// 当前 num 个奖品，该小时可以获得几个 这样写更容易懂 ( num / 100 * chanceNum)
			n := num * chanceNum / 100
			// 最终分配的数量，赋值
			hourData[h] = n
			// 计数加 n ： 这里其实其实不用这样， 因为都是按照 100 个数，按照PrizeDataRandomDayTime
			// 权值分配给 24小时的，那么剩余的数量， 一定是 num % 100 ，得到的是小于 100 的一个数。
			// 直接使用 num % 100 计算即可
			hourNum += n
		}
		num -= hourNum
	}
	// 处理剩余的，随机处理
	if num > 0 {
		num--
		// 获取一个 0-100 的值，计算落到每小时的概率
		hourIndex := comm.Random(100)
		h := conf.PrizeDataRandomDayTime[hourIndex]
		hourData[h]++
	}
	// 下面将 每小时内的奖品数量分配到 60 分钟
	for h, hNum := range hourData {
		if hNum <= 0 {
			continue
		}
		minuteData := [60]int{}
		// 类似上面的计算，当数量大于 60，先精确
		if hNum >= 60 {
			avgMinute := hNum / 60
			for i := 0; i < 60; i++ {
				minuteData[i] = avgMinute
			}
			hNum -= avgMinute * 60
		}
		// hNum 可能还有剩余, 接着随机
		for hNum > 0 {
			hNum--
			// 随机分钟
			m := comm.Random(60)
			minuteData[m]++
		}
		rs[h] = minuteData
	}
	return rs
}

// 将每天、每小时、每分钟的奖品数量，格式化成具体到一个时间（分钟）的奖品数量
// 结构为： [day][hour][minute]num (PrizeData)
// nowTime: 当前时间（类型为时间戳）， dayNum： 发奖周期
func formatGiftPrizeData(nowTime, dayNum int, PrizeData map[int]map[int][60]int) [][2]int {
	// 定义结果集数据
	rs := make([][2]int, 0)
	// 获取当前时间对应的小时
	nowHour := time.Now().Hour()
	// 处理日期的数据
	// PrizeData 是map类型，for 循环使数据遍历有序，从 0 ~ dayNum - 1 取天。
	for dn := 0; dn < dayNum; dn++ {
		// 获取每天的数据： map[int][60]int  /小时/分钟/数量
		dayData, ok := PrizeData[dn]
		if !ok {
			continue
		}
		// 处理小时的数据
		dayTime := nowTime + dn*86400
		for hn := 0; hn < 24; hn++ {
			// 获取每小时的数据  [60]int  分钟/数量
			// 假如当前时间是 18点，那么应该从18点开始计算。(hn + nowHour) % 24 从当前时间点往后推24小时（1天）
			hourData, ok := dayData[(hn+nowHour)%24]
			if !ok {
				continue
			}
			// 处理分钟的数据
			hourTime := dayTime + hn*3600
			for mn := 0; mn < 60; mn++ {
				num := hourData[mn]
				if num <= 0 {
					continue
				}
				minuteTime := hourTime + mn*60
				rs = append(rs, [2]int{minuteTime, num})
			}
		}
	}
	return rs
}

// 自动填充奖品池的服务
func DistributionGiftPool() int {
	totalNum := 0
	now := comm.NowUnix()
	giftService := services.NewGiftService()
	// 不使用缓存读， 后台数据的读取频率不高，直接从数据库中进行读取。
	list := giftService.GetAll(false)
	if list != nil && len(list) > 0 {
		for _, gift := range list {
			// 必须是正常状态的奖品
			if gift.SysStatus != 0 {
				continue
			}
			// 必须是数量有限的
			if gift.PrizeNum < 1 {
				continue
			}
			// 要在奖品时间范围内
			if gift.TimeBegin > now || gift.TimeEnd < now {
				continue
			}
			// 发奖计划 是否正确
			if len(gift.PrizeData) <= 7 {
				continue
			}
			// 将PrizeData数据反序列化
			var cronData [][2]int
			err := json.Unmarshal([]byte(gift.PrizeData), &cronData)
			if err != nil {
				log.Println("prizedata.DistributionGiftPool Unmarshal error = ", err)
			} else {
				index := 0
				//  统计数量的信息
				giftNum := 0
				for i, data := range cronData {
					// 时间： 奖品数
					ct := data[0]
					num := data[1]
					if ct <= now {
						giftNum += num
						index = i + 1
					} else {
						break
					}
				}
				// 更新奖品池
				if giftNum > 0 {
					incrGiftPool(gift.Id, giftNum)
					totalNum += giftNum
				}
				// 更新奖品的发奖计划
				if index > 0 {
					// 如果偏移达到了最后的位置， 赋值为 0
					if index >= len(cronData) {
						cronData = make([][2]int, 0)
					} else {
						cronData = cronData[index:]
					}
					// 序列化， 更新到数据库
					str, err := json.Marshal(cronData)
					if err != nil {
						log.Println("prizedata.DistributionGiftPool Marshal ",
							cronData, "error = ", err)
					}
					columns := []string{"prize_data"}
					err = giftService.Update(&models.LtGift{
						Id:        gift.Id,
						PrizeData: string(str),
					}, columns)
					if err != nil {
						log.Println("prizedata.DistributionGiftPool giftService.Update error = ", err)
					}
				}
			}
		}
	}
	return totalNum
}

// 并发的增加奖品池库存，二次补充库存
func incrGiftPool(id, num int) int {
	key := "gift_pool"
	cacheObj := datasource.InstanceCache()
	// 返回的是数量
	rtNum, err := redis.Int64(cacheObj.Do("HINCRBY", key, id, num))
	if err != nil {
		log.Println("prizedata.IcrGiftPool1 error = ", err)
		return 0
	}
	// 二次扩充，如果当前的结果还小于添加的数量
	if int(rtNum) < num {
		// 递增比预期值要小
		num2 := num - int(rtNum)
		rtNum, err = redis.Int64(cacheObj.Do("HINCRBY", key, id, num2))
		if err != nil {
			log.Println("prizedata.IcrGiftPool2 error = ", err)
			return 0
		}
		// (ps: 如果觉得二次补充还可能出现问题，那就将这里改为递归执行~)
	}
	return int(rtNum)
}
